package auth

import (
	"bytes"
	"crypto/subtle"
	"encoding/json"
	"net"
	"sync"
	"time"

	"gitee.com/zfd81/dbrs/sys"

	"vitess.io/vitess/go/mysql"

	"vitess.io/vitess/go/vt/log"
	querypb "vitess.io/vitess/go/vt/proto/query"
	"vitess.io/vitess/go/vt/proto/vtrpc"
	"vitess.io/vitess/go/vt/vterrors"
)

const (
	localhostName = "localhost"
)

// AuthServerStatic implements AuthServer using a static configuration.
type AuthServerStatic struct {
	methods []mysql.AuthMethod
	// This mutex helps us prevent data races between the multiple updates of entries.
	mu     sync.Mutex
	ticker *time.Ticker
}

// AuthServerStaticEntry stores the values for a given user.
type AuthServerStaticEntry struct {
	// MysqlNativePassword is generated by password hashing methods in MySQL.
	// These changes are illustrated by changes in the result from the PASSWORD() function
	// that computes password hash values and in the structure of the user table where passwords are stored.
	// mysql> SELECT PASSWORD('mypass');
	// +-------------------------------------------+
	// | PASSWORD('mypass')                        |
	// +-------------------------------------------+
	// | *6C8989366EAF75BB670AD8EA7A7FC1176A95CEF4 |
	// +-------------------------------------------+
	// MysqlNativePassword's format looks like "*6C8989366EAF75BB670AD8EA7A7FC1176A95CEF4", it store a hashing value.
	// Use MysqlNativePassword in auth config, maybe more secure. After all, it is cryptographic storage.
	MysqlNativePassword string
	Password            string
	UserData            string
	SourceHost          string
	Groups              []string
}

// NewAuthServerStatic returns a new empty AuthServerStatic.
func NewAuthServerStatic(file, jsonConfig string, reloadInterval time.Duration) *AuthServerStatic {
	a := &AuthServerStatic{}

	a.methods = []mysql.AuthMethod{mysql.NewMysqlNativeAuthMethod(a, a)}

	return a
}

// NewAuthServerStaticWithAuthMethodDescription returns a new empty AuthServerStatic
// but with support for a different auth method. Mostly used for testing purposes.
func NewAuthServerStaticWithAuthMethodDescription(file, jsonConfig string, reloadInterval time.Duration, authMethodDescription mysql.AuthMethodDescription) *AuthServerStatic {
	a := &AuthServerStatic{}

	var authMethod mysql.AuthMethod
	switch authMethodDescription {
	case mysql.CachingSha2Password:
		authMethod = mysql.NewSha2CachingAuthMethod(a, a, a)
	case mysql.MysqlNativePassword:
		authMethod = mysql.NewMysqlNativeAuthMethod(a, a)
	case mysql.MysqlClearPassword:
		authMethod = mysql.NewMysqlClearAuthMethod(a, a)
	case mysql.MysqlDialog:
		authMethod = mysql.NewMysqlDialogAuthMethod(a, a, "")
	}

	a.methods = []mysql.AuthMethod{authMethod}

	return a
}

// HandleUser is part of the Validator interface. We
// handle any user here since we don't check up front.
func (a *AuthServerStatic) HandleUser(user string) bool {
	return true
}

// UserEntryWithPassword implements password lookup based on a plain
// text password that is negotiated with the client.
func (a *AuthServerStatic) UserEntryWithPassword(conn *mysql.Conn, user string, password string, remoteAddr net.Addr) (mysql.Getter, error) {
	//a.mu.Lock()
	//entries, ok := a.entries[user]
	//a.mu.Unlock()
	//
	//if !ok {
	//	return &StaticUserData{}, mysql.NewSQLError(mysql.ERAccessDeniedError, mysql.SSAccessDeniedError, "Access denied for user '%v'", user)
	//}
	//
	//for _, entry := range entries {
	//	// Validate the password.
	//	if MatchSourceHost(remoteAddr, entry.SourceHost) && subtle.ConstantTimeCompare([]byte(password), []byte(entry.Password)) == 1 {
	//		return &StaticUserData{entry.UserData, entry.Groups}, nil
	//	}
	//}
	return &StaticUserData{}, mysql.NewSQLError(mysql.ERAccessDeniedError, mysql.SSAccessDeniedError, "Access denied for user '%v'", user)
}

// UserEntryWithHash implements password lookup based on a
// mysql_native_password hash that is negotiated with the client.
func (a *AuthServerStatic) UserEntryWithHash(conn *mysql.Conn, salt []byte, user string, authResponse []byte, remoteAddr net.Addr) (mysql.Getter, error) {
	a.mu.Lock()
	a.mu.Unlock()

	userinfo, err := sys.FindUser(user)
	if err != nil {
		return &StaticUserData{}, err
	}
	if userinfo == nil {
		return &StaticUserData{}, mysql.NewSQLError(mysql.ERAccessDeniedError, mysql.SSAccessDeniedError, "Access denied for user '%v'", user)
	}

	entries := []*AuthServerStaticEntry{
		{
			MysqlNativePassword: NativePassword(userinfo.Password),
			Password:            NativePassword(userinfo.Password),
		},
	}

	for _, entry := range entries {
		if entry.MysqlNativePassword != "" {
			hash, err := mysql.DecodeMysqlNativePasswordHex(entry.MysqlNativePassword)
			if err != nil {
				return &StaticUserData{entry.UserData, entry.Groups}, mysql.NewSQLError(mysql.ERAccessDeniedError, mysql.SSAccessDeniedError, "Access denied for user '%v'", user)
			}

			isPass := mysql.VerifyHashedMysqlNativePassword(authResponse, salt, hash)
			if MatchSourceHost(remoteAddr, entry.SourceHost) && isPass {
				return &StaticUserData{entry.UserData, entry.Groups}, nil
			}
		} else {
			computedAuthResponse := mysql.ScrambleMysqlNativePassword(salt, []byte(entry.Password))
			// Validate the password.
			if MatchSourceHost(remoteAddr, entry.SourceHost) && subtle.ConstantTimeCompare(authResponse, computedAuthResponse) == 1 {
				return &StaticUserData{entry.UserData, entry.Groups}, nil
			}
		}
	}
	return &StaticUserData{}, mysql.NewSQLError(mysql.ERAccessDeniedError, mysql.SSAccessDeniedError, "Access denied for user '%v'", user)
}

// UserEntryWithCacheHash implements password lookup based on a
// caching_sha2_password hash that is negotiated with the client.
func (a *AuthServerStatic) UserEntryWithCacheHash(conn *mysql.Conn, salt []byte, user string, authResponse []byte, remoteAddr net.Addr) (mysql.Getter, mysql.CacheState, error) {
	//a.mu.Lock()
	//entries, ok := a.entries[user]
	//a.mu.Unlock()
	//
	//if !ok {
	//	return &StaticUserData{}, mysql.AuthRejected, mysql.NewSQLError(mysql.ERAccessDeniedError, mysql.SSAccessDeniedError, "Access denied for user '%v'", user)
	//}
	//
	//for _, entry := range entries {
	//	computedAuthResponse := mysql.ScrambleCachingSha2Password(salt, []byte(entry.Password))
	//
	//	// Validate the password.
	//	if MatchSourceHost(remoteAddr, entry.SourceHost) && subtle.ConstantTimeCompare(authResponse, computedAuthResponse) == 1 {
	//		return &StaticUserData{entry.UserData, entry.Groups}, mysql.AuthAccepted, nil
	//	}
	//}
	return &StaticUserData{}, mysql.AuthRejected, mysql.NewSQLError(mysql.ERAccessDeniedError, mysql.SSAccessDeniedError, "Access denied for user '%v'", user)
}

// AuthMethods returns the AuthMethod instances this auth server can handle.
func (a *AuthServerStatic) AuthMethods() []mysql.AuthMethod {
	return a.methods
}

// DefaultAuthMethodDescription returns the default auth method in the handshake which
// is MysqlNativePassword for this auth server.
func (a *AuthServerStatic) DefaultAuthMethodDescription() mysql.AuthMethodDescription {
	return mysql.MysqlNativePassword
}

// ParseConfig takes a JSON MySQL static config and converts to a validated map
func ParseConfig(jsonBytes []byte, config *map[string][]*AuthServerStaticEntry) error {
	decoder := json.NewDecoder(bytes.NewReader(jsonBytes))
	decoder.DisallowUnknownFields()
	if err := decoder.Decode(config); err != nil {
		// Couldn't parse, will try to parse with legacy config
		return parseLegacyConfig(jsonBytes, config)
	}
	return validateConfig(*config)
}

func parseLegacyConfig(jsonBytes []byte, config *map[string][]*AuthServerStaticEntry) error {
	// legacy config doesn't have an array
	legacyConfig := make(map[string]*AuthServerStaticEntry)
	decoder := json.NewDecoder(bytes.NewReader(jsonBytes))
	decoder.DisallowUnknownFields()
	if err := decoder.Decode(&legacyConfig); err != nil {
		return err
	}
	log.Warningf("Config parsed using legacy configuration. Please update to the latest format: {\"user\":[{\"Password\": \"xxx\"}, ...]}")
	for key, value := range legacyConfig {
		(*config)[key] = append((*config)[key], value)
	}
	return nil
}

func validateConfig(config map[string][]*AuthServerStaticEntry) error {
	for _, entries := range config {
		for _, entry := range entries {
			if entry.SourceHost != "" && entry.SourceHost != localhostName {
				return vterrors.Errorf(vtrpc.Code_INVALID_ARGUMENT, "invalid SourceHost found (only localhost is supported): %v", entry.SourceHost)
			}
		}
	}
	return nil
}

// MatchSourceHost validates host entry in auth configuration
func MatchSourceHost(remoteAddr net.Addr, targetSourceHost string) bool {
	// Legacy support, there was not matcher defined default to true
	if targetSourceHost == "" {
		return true
	}
	switch remoteAddr.(type) {
	case *net.UnixAddr:
		if targetSourceHost == localhostName {
			return true
		}
	}
	return false
}

// StaticUserData holds the username and groups
type StaticUserData struct {
	Username string
	Groups   []string
}

// Get returns the wrapped username and groups
func (sud *StaticUserData) Get() *querypb.VTGateCallerID {
	return &querypb.VTGateCallerID{Username: sud.Username, Groups: sud.Groups}
}
